package com.atguigu.gmall.cart.service.impl;

import com.atguigu.gmall.cart.mapper.CartInfoMapper;
import com.atguigu.gmall.cart.service.CartInfoAsyncService;
import com.atguigu.gmall.cart.service.CartInfoService;
import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.common.exception.GmallException;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.AuthContextHolder;
import com.atguigu.gmall.model.cart.CartInfo;
import com.atguigu.gmall.model.product.SkuInfo;
import com.atguigu.gmall.product.client.feign.ProductFeignClient;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author wk
 * @create 2021-09-07 19:18
 * 购物车模块 使用双写 同时写入购物车和数据库 不设置过期时间
 * 查询直接可以查询缓存 效率高
 * <p>
 * 优化方案：
 * 加入DB使用异步
 * 加入缓存使用同步
 */
@Slf4j
@Service
public class CartInfoServiceImpl implements CartInfoService {
    @Autowired
    private CartInfoMapper cartInfoMapper;
    @Autowired
    private ProductFeignClient productFeignClient;
    // 使用多线程优化
    @Autowired
    private CartInfoAsyncService cartInfoAsyncService;
    @Autowired
    private RedisTemplate redisTemplate;

    // 添加到购物车
    @Override
    @Transactional
    public void addCart(Long skuId, Integer skuNum, String userId) {
        // String userId = "3";
        // 1.先要从redis中查询 如果当前商品存在 那么就对其进行更新(数量)
        // 购物车在redis中存储数据的格式使用hash类似于hashMap
        // 可以设置user:userId:cart 设置为键 保障每个用户使用自己的购物车
        // 设置skuId商品id作为键确定一款商品 CartInfo是商品的详细信息
        String cacheKey = RedisConst.USER_KEY_PREFIX + userId + RedisConst.USER_CART_KEY_SUFFIX;
        CartInfo cartInfo = (CartInfo) redisTemplate.opsForHash().get(cacheKey, skuId.toString());
        if (cartInfo != null) {
            // 如果存在 则更新缓存和数据库
            cartInfo.setSkuNum(cartInfo.getSkuNum() + skuNum);
            // 同时当商品添加后来到购物车后 将该商品选中
            cartInfo.setIsChecked(CartInfo.IS_CHECKED);
            // 更新数据库
            // cartInfoMapper.updateById(cartInfo);
            // 异步更新数据库
            // 避免因为没有商品id 更新数据库 可以使用userId+skuId共同作为条件更新数据库
            cartInfoAsyncService.updateAsync(cartInfo);
            log.info("主线程" + Thread.currentThread().getName());
        } else {
            // 如果缓存中不存在 则开始添加
            cartInfo = new CartInfo();
            // 1.需要远程调用获取商品库存信息
            SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
            if (skuInfo != null && skuInfo.getId() != null) {
                cartInfo.setSkuId(skuId);
                cartInfo.setSkuName(skuInfo.getSkuName());
                cartInfo.setImgUrl(skuInfo.getSkuDefaultImg());
                cartInfo.setIsChecked(CartInfo.IS_CHECKED);
                cartInfo.setSkuNum(skuNum);
                cartInfo.setUserId(userId);
                // 远程调用
                BigDecimal skuPrice = productFeignClient.getSkuPrice(skuId);
                // 设置加入购物车时的价格
                cartInfo.setCartPrice(skuPrice);
                // 库存中的商品价格
                // 此处无需加入 每次去购物车时 都会进行远程调用从数据库中查询实时价格并设置
                cartInfo.setSkuPrice(skuPrice);
                // 插入数据库
                // cartInfoMapper.insert(cartInfo);
                // 使用多线程进行优化 数据库使用异步操作
                cartInfoAsyncService.insertAsync(cartInfo);
                log.info("主线程" + Thread.currentThread().getName());
            } else {
                throw new GmallException(ResultCodeEnum.FAIL);
            }
        }
        // 无论插入还是更新 最终缓存的操作都一样  其中如果已存在则根据键 更新值
        redisTemplate.opsForHash().put(cacheKey, skuId.toString(), cartInfo);
        System.out.println(cartInfo);
    }

    @Override
    public CartInfo findCartInfo(Long skuId) {
        // String userId="3";
        // 方式一 在微服务模块 使用 HttpServletRequest request  String userId = AuthContextHolder.getUserId(request);
        // 然后在调用处添加参数userId
        // 方式二 直接获取
        String userId = "";
        ServletRequestAttributes servletRequestAttributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (servletRequestAttributes != null) {
            HttpServletRequest request = servletRequestAttributes.getRequest();
            if (request != null) {
                userId = AuthContextHolder.getUserId(request);
                if (StringUtils.isEmpty(userId)) {
                    // 如果没有用户id 则暂时使用临时id作为填补
                    userId = AuthContextHolder.getUserTempId(request);
                }
            }
        }
        String cacheKey = RedisConst.USER_KEY_PREFIX + userId + RedisConst.USER_CART_KEY_SUFFIX;
        // 有一种情况是 用户在未登录之前 使用临时id添加了商品到购物车
        // 登录后 在购物车中不会有使用临时id添加的商品信息
        // 那么就设置先根据用户id查询是否有某件商品信息
        if (redisTemplate.opsForHash().hasKey(cacheKey, skuId.toString())) {
            // 判断真实用户id下有没有对应的商品
            return (CartInfo) redisTemplate.opsForHash().get(cacheKey, skuId.toString());
        } else {
            // 如果真实用户id没有商品信息 那么就使用临时用户id查询 临时用户购物车列表
            HttpServletRequest request = servletRequestAttributes.getRequest();
            String cacheKeyByUserTempId = RedisConst.USER_KEY_PREFIX
                    + AuthContextHolder.getUserTempId(request)
                    + RedisConst.USER_CART_KEY_SUFFIX;
            return (CartInfo) redisTemplate.opsForHash().get(cacheKeyByUserTempId, skuId.toString());
        }
    }

    // 获取购物车列表
    @Transactional
    @Override
    public List<CartInfo> cartList(String userId, String userTempId) {
        // 1.判断真实用户是否登录
        if (!StringUtils.isEmpty(userId)) {
            // 已登录 又存在两种情况
            if (!StringUtils.isEmpty(userTempId)) {
                //  临时用户加入了购物车又进行了删除
                // 第二种  加入购物车时 未登录 使用的是临时用户id进行的加入 然后再登录
                return mergeCartInfoList(userId, userTempId);
            } else {
                // 第一种 加入购物车时 已经登录 也就是没有使用临时用户id进行加入购物车
                // 直接返回 用户id对应的购物车列表
                return getCartInfoList(userId);
            }
        } else {
            // 为空 说明未登录 则使用临时用户id 查询临时购物车
            return getCartInfoList(userTempId);
        }
    }

    // 更新购物车商品选中状态
    @Override
    public void checkCart(Long skuId, Integer isChecked, String userId) {
        CartInfo cartInfo = new CartInfo();
        // 更新数据库
        cartInfo.setIsChecked(isChecked);
        cartInfoMapper.update(cartInfo, new QueryWrapper<CartInfo>()
                .eq("user_id", userId)
                .eq("sku_id", skuId));
        // 更新缓存
        String cacheKey = RedisConst.USER_KEY_PREFIX
                + userId
                + RedisConst.USER_CART_KEY_SUFFIX;
        // 先查
        cartInfo = (CartInfo) redisTemplate.opsForHash().get(cacheKey, skuId.toString());
        cartInfo.setIsChecked(isChecked);
        redisTemplate.opsForHash().put(cacheKey, skuId.toString(), cartInfo);
    }

    // 查询商品清单信息即 从缓存中查询被选中的商品信息
    @Override
    public List<CartInfo> findCartInfoListByIsChecked(String userId) {
        String cacheKey = RedisConst.USER_KEY_PREFIX
                + userId
                + RedisConst.USER_CART_KEY_SUFFIX;
        // 购物车所有的商品集合
        // TODO 思考优化 等会做
        List<CartInfo> values = redisTemplate.opsForHash().values(cacheKey);
        // 过滤被选中的商品 过滤后再继续设置实时价格并返回
        return values.stream().filter(cartInfo -> {
            // 说明被选中了
            if (1 == cartInfo.getIsChecked()) {
                return true;
            }
            return false;
        }).peek(cartInfo -> {
            // 更新实时价格
            BigDecimal skuPrice = productFeignClient.getSkuPrice(cartInfo.getSkuId());
            cartInfo.setSkuPrice(skuPrice);
        }).collect(Collectors.toList());
    }

    // 是以临时用户的id加入的购物车  登录后需要将临时用户对应的购物车列表加到登录的用户购物车列表
    // 同时将临时用户对应的购物车列表清空
    private List<CartInfo> mergeCartInfoList(String userId, String userTempId) {
        // 获取购物车列表
        List<CartInfo> cartInfoListByUserId = getCartInfoList(userId);
        List<CartInfo> cartInfoListByUserTempId = getCartInfoList(userTempId);
        Map<Long, CartInfo> cartInfoMapByUserId = new HashMap<>();
        // 判断临时用户购物车列表
        // 临时用户购物车列表存在
        if (!CollectionUtils.isEmpty(cartInfoListByUserTempId)) {
            // 真实用户的购物车列表存在
            if (!CollectionUtils.isEmpty(cartInfoListByUserId)) {
                    /* 普通版本
                    * Map<Long, CartInfo> cartInfoMapByUserId = cartInfoListByUserId.stream().collect(Collectors.
                     toMap(cartInfoByUserId -> {return cartInfoByUserTempId.getSkuId();}, cartInfoByUserId -> {return cartInfoByUserId;}));
                    * */
                // 将用户的购物车列表转换为map 以商品id为键 每一件商品信息为值
                // 然后再通过 map集合的键去比较 真实用户和临时用户的购物车中是否存在同一款商品
                cartInfoMapByUserId = cartInfoListByUserId.stream()
                        .collect(Collectors.toMap(CartInfo::getSkuId, cartInfoByUserId -> cartInfoByUserId));
                for (CartInfo cartInfoByUserTempId : cartInfoListByUserTempId) {
                    // 1.如果临时用户的购物车中商品在 真实用户购物车中已存在
                    // 根据临时用户的购物车中商品id与真实用户的购物车中的商品进行比较
                    CartInfo cartInfoByUserId = cartInfoMapByUserId.get(cartInfoByUserTempId.getSkuId());
                    if (!StringUtils.isEmpty(cartInfoByUserId)) {
                        // 1.1那么就把用户购物车中已存在的商品数量进行更新
                        cartInfoByUserId.setSkuNum(cartInfoByUserId.getSkuNum() + cartInfoByUserTempId.getSkuNum());
                        // 1.1.2同时把数量更新的商品进行选中
                        cartInfoByUserId.setIsChecked(CartInfo.IS_CHECKED);
                        // 1.1.3更新数据库  先数据库 再缓存 添加事务注解 保证数据一致性 可回滚
                        cartInfoMapper.update(cartInfoByUserId, new QueryWrapper<CartInfo>()
                                .eq("sku_id", cartInfoByUserId.getSkuId())
                                .eq("user_id", cartInfoByUserId.getUserId())
                        );
                        // 1.1.4将临时用户的购物车进行清空
                        cartInfoMapper.delete(new QueryWrapper<CartInfo>()
                                .eq("sku_id", cartInfoByUserTempId.getSkuId())
                                .eq("user_id", cartInfoByUserTempId.getUserId())
                        );
                        // 1.1.5更新缓存
                        String cacheKey = RedisConst.USER_KEY_PREFIX
                                + userId
                                + RedisConst.USER_CART_KEY_SUFFIX;
                        redisTemplate.opsForHash().put(cacheKey, cartInfoByUserId.getSkuId().toString(), cartInfoByUserId);
                    } else {
                        // 1.2如果不存在 那就把临时用户购物车列表追加到真实用户列表中
                        cartInfoMapByUserId.put(cartInfoByUserTempId.getSkuId(), cartInfoByUserTempId);
                        // 1.2.1更新数据库 意味着只要将用户id修改为真实用户id即可
                        cartInfoByUserTempId.setUserId(userId);
                        cartInfoMapper.update(cartInfoByUserTempId, new QueryWrapper<CartInfo>()
                                .eq("user_id", userTempId)
                                .eq("sku_id", cartInfoByUserTempId.getSkuId())
                        );
                        // 1.2.2更新缓存
                        String cacheKey = RedisConst.USER_KEY_PREFIX
                                + userId
                                + RedisConst.USER_CART_KEY_SUFFIX;
                        redisTemplate.opsForHash().put(cacheKey, cartInfoByUserTempId.getSkuId().toString(), cartInfoByUserTempId);
                    }
                }
            } else {
                // 此处应该持久化到缓存和数据库中
                // 1.更新数据库
                // 直接更新临时用户id修改为真实用户id
                for (CartInfo cartInfo : cartInfoListByUserTempId) {
                    cartInfo.setUserId(userId);
                    cartInfoMapper.update(cartInfo, null);
                }
                // 2.设置缓存
                /*String cacheKey = RedisConst.USER_KEY_PREFIX
                        + userId
                        + RedisConst.USER_CART_KEY_SUFFIX;
                for (CartInfo cartInfo : cartInfoListByUserTempId) {
                    cartInfo.setUserId(userId);
                    redisTemplate.opsForHash().put(cacheKey, cartInfo.getSkuId().toString(), cartInfo);
                }*/

                // 优化 批量进行更新
                // peek循环遍历 设置每一个userId
                Map<String, CartInfo> cartInfoMap = cartInfoListByUserTempId.stream().peek(cartInfo -> {
                    // 设置每个用户id
                    cartInfo.setUserId(userId);
                }).collect(Collectors.toMap(
                        cartInfo1 -> cartInfo1.getSkuId().toString(), cartInfo -> cartInfo));
                String cacheKeyCartByUserId = RedisConst.USER_KEY_PREFIX + userId
                        + RedisConst.USER_CART_KEY_SUFFIX;
                redisTemplate.opsForHash().putAll(cacheKeyCartByUserId, cartInfoMap);
                // 真实用户的购物车列表不存在 则直接可以将临时用户的购物车列表返回
                return cartInfoListByUserTempId;
            }
            // 将临时用户的购物车缓存列表进行清除
            String cacheKey = RedisConst.USER_KEY_PREFIX
                    + userTempId
                    + RedisConst.USER_CART_KEY_SUFFIX;
            redisTemplate.delete(cacheKey);
            // 最后将真实用户的购物车列表进行返回
            return new ArrayList<>(cartInfoMapByUserId.values());
        } else {
            // 临时用户购物车列表不存在 则可以直接将用户的购物车列表进行返回
            return cartInfoListByUserId;
        }
    }

    // 返回购物车列表
    private List<CartInfo> getCartInfoList(String userId) {
        // 从缓存中获取
        String cacheKey = RedisConst.USER_KEY_PREFIX + userId + RedisConst.USER_CART_KEY_SUFFIX;
        // 获取购物车列表 获取 缓存中value列表
        List<CartInfo> cartInfoList = redisTemplate.opsForHash().values(cacheKey);
        // 页面上直接进入购物车的按钮 当购物车为空时 使用cartInfoList可能会报空指针异常
        if (CollectionUtils.isEmpty(cartInfoList)) {
            cartInfoList.stream().forEach(cartInfo -> {
                // 远程调用  设置实时价格
                cartInfo.setSkuPrice(productFeignClient.getSkuPrice(cartInfo.getSkuId()));
            });
        }
        return cartInfoList;
    }
}
